﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Noris.Tools.FrxEditor.Data
{
    #region class Reverter : třída, která řeší Undo/Redo
    /// <summary>
	/// Reverter : třída, která řeší Undo/Redo.
    /// Obsahuje v sobě paměť stavů, registruje změny stavů, umožňuje provádět Undo a Redo (vyvoláním svých metod), spolupracuje s ukládanými objekty (IRevertItem) a s jejich containerem (IRevertContainer).
	/// </summary>
	public class Reverter
    {
        #region Konstrukce
		/// <summary>
		/// Reverter : třída, která řeší Undo/Redo.
		/// Konstruktor.
		/// </summary>
		/// <param name="revertContainer">Container objektů, pro které zde provádíme Undo a Redo</param>
		public Reverter(IRevertContainer revertContainer)
        {
			this.RevertContainer = revertContainer;
			this.StepPointer = 0;
            this.RevertSteps = new List<RevertStep>();
        }
		/// <summary>
		/// Container objektů, pro které zde provádíme Undo a Redo
		/// </summary>
		private IRevertContainer RevertContainer;
		/// <summary>
		/// Ukazatel do seznamu na pozici, z níž se bude číst nejbližší REDO záznam.
		/// Význam hodnot:
		/// Po vložení (Store) prvního záznamu = na index [0] do pole Items 
		/// bude ukazovat StepPointer za tento záznam = bude mít hodnotu 1 (vždy po Store bude obsahovat Items.Count).
		/// UNDO je povoleno jen tehdy, pokud StepPointer je větší než 0.
		/// Požadavek na UNDO nejprve sníží StepPointer, z té pozice vyzvedne záznam o změnách, a do všech objektů vepíše data Old.
		/// REDO je povoleno tehdy, pokud StepPointer je menší než (Items.Count).
		/// Požadavek na REDO z aktuální pozice vyzvedne data New, uloží je do objektů a pak zvýší StepPointer.
		/// Při Store dat, když Pointer je menší než Count (tj. je dostupné REDO) se zahodí všechny objekty s indexem rovným pointeru a vyšší.
		/// Pokud tedy máme 4 pozice (0,1,2,3) (Pointer == Count == 4), 
		/// z nichž pro poslední dáme UNDO (Pointer jde z 4 na 3, z pozice [3] načteme data Old), 
		/// a nyní dáme Store, pak data na pozici [3] odebereme, včetně dat následujících (v počtu Count - Pointer)
		/// </summary>
        private int StepPointer;
		/// <summary>
		/// Záznamy jednotlivých kroků Undo/Redo
		/// </summary>
        private List<RevertStep> RevertSteps;
        #endregion
        #region Public property
        /// <summary>
        /// Můžeme udělat Undo?
        /// </summary>
        public bool CanUndo { get { return this.StepPointer > 0; } }
        /// <summary>
        /// Můžeme udělat Redo?
        /// </summary>
        public bool CanRedo { get { return this.StepPointer < this.RevertSteps.Count; } }
        #endregion
        #region Public metody - Store, Clear()
		/// <summary>
		/// Přidání nové položky.
		/// </summary>
		/// <param name="iRevert"></param>
		public void StoreAdd(IRevertItem iRevert)
		{
			if (iRevert == null) return;
			this.StoreAdd(new List<IRevertItem>() { iRevert });
		}
		/// <summary>
		/// Přidání nových položek.
		/// </summary>
		/// <param name="iRevertItems"></param>
		public void StoreAdd(IEnumerable<IRevertItem> iRevertItems)
		{
			if (iRevertItems == null) return;
			List<RevertOne> changes = new List<RevertOne>();
			foreach (IRevertItem iRevert in iRevertItems)
			{
				if (iRevert != null)
					this.AddRevertOne(RevertOne.CreateAdd(iRevert), changes);
			}
			this.RevertItemAdd(changes);
			this.PreparedItems = null;
		}
		/// <summary>
		/// Příprava na změnu.
		/// Volá se před zahájením změn, typicky v okamžiku po výběru objektů před zahájením editace.
		/// Reverter si uloží jejich seznam a uloží si jejich aktuální stav.
		/// Pokud následně bude volána metoda StoreChange(items), pak se ta budoucí změna uloží (se stavem Old = stav v době volání metody StoreChangePrepare()).
		/// Pokud následně bude volána metoda StoreRemove(), pak se uloží změna typu Remove pro všechny připravené objekty.
		/// Vedle toho ale existuje i metoda StoreRemove(items), ta uloží info o odebrání explicitně předaných objektů.
		/// </summary>
        /// <param name="iRevert">Položka, které se budoucí změna může týkat.</param>
		public void StoreChangeBefore(IRevertItem iRevert)
		{
			if (iRevert == null) return;
			this.StoreChangeBefore(new List<IRevertItem>() { iRevert });
		}
		/// <summary>
        /// Příprava na změnu.
        /// Volá se před zahájením změn, typicky v okamžiku po výběru objektů před zahájením editace.
        /// Reverter si uloží jejich seznam a uloží si jejich aktuální stav.
        /// Pokud následně bude volána metoda StoreChange(items), pak se ta budoucí změna uloží (se stavem Old = stav v době volání metody StoreChangePrepare()).
        /// Pokud následně bude volána metoda StoreRemove(), pak se uloží změna typu Remove pro všechny připravené objekty.
        /// Vedle toho ale existuje i metoda StoreRemove(items), ta uloží info o odebrání explicitně předaných objektů.
        /// </summary>
        /// <param name="iRevertItems">Položky, jichž se budoucí změna může týkat.</param>
        public void StoreChangeBefore(IEnumerable<IRevertItem> iRevertItems)
        {
			if (iRevertItems == null) return;
			List<RevertOne> changes = new List<RevertOne>();
            foreach (IRevertItem iRevert in iRevertItems)
            {
				if (iRevert != null)
					this.AddRevertOne(RevertOne.CreateModifyBefore(iRevert), changes);
            }
			this.PreparedItems = changes;
		}
		/// <summary>
        /// Provedení změn.
        /// Volá se při (po) provedení změn v objektech, typicky v okamžiku po setování nové hodnoty do objektu.
        /// Před touto metodou měla být volána metoda StoreChangePrepare() s předáním objektů ve stavu Před změnou.
        /// Reverter si najde objekty před změnou a po změně, založí záznam o změně (pro všechny objekty do jednoho záznamu) a vloží do něj data o změnách.
        /// </summary>
        /// <param name="iRevertItems">Položky, na nichž došlo ke změně.</param>
        public void StoreChangeAfter(IEnumerable<IRevertItem> iRevertItems)
        {
			if (iRevertItems == null) return;
			List<RevertOne> changes = new List<RevertOne>();
            foreach (IRevertItem iRevert in iRevertItems)
            {
				if (iRevert != null)
				{
					// Položka iRevert obsahuje data některého objektu po změně.
					// Abych mohl zapsat korektní informaci, potřebuji najít data téhož objektu před změnou, v poli this.PreparedItems:
					RevertOne preparedItem = this.FindPreparedItem(iRevert);
					if (preparedItem != null)
						this.AddRevertOne(RevertOne.CreateModifyAfter(preparedItem, iRevert), changes);
				}
            }
			this.RevertItemAdd(changes);              // Aktuální změny (ze stavu Old na New) zapíšu jako další položku UNDO

			// Soupis this.PreparedItems nebudu měnit, můžou přijít další změny.
			// V patřičných objektech (položky tohoto seznamu) jsou v případě potřeby provedeny změny: do Old je vložen aktuální stav.
			//  To řeší metoda RevertItemChange.CreateModifyAfter(preparedItem, iRevert).
        }
        /// <summary>
        /// Odebrání položek, které jsou aktuálně připravené.
        /// </summary>
        public void StoreRemove()
        {
            if (this.PreparedItems == null)
                throw new InvalidOperationException("Nelze volat Reverter.StoreRemove() v situaci, kdy nejsou připraveny položky v PreparedItems !");
            
            List<RevertOne> changes = new List<RevertOne>();
			foreach (RevertOne itemChange in this.PreparedItems)
				this.AddRevertOne(RevertOne.CreateRemove(itemChange), changes);
            this.RevertItemAdd(changes);

            this.PreparedItems = null;
        }
        /// <summary>
        /// Odebrání položek.
        /// Volá se tehdy, když uživatel odebírá některé položky (nezáleží na tom, zda jsou z IRevertContainer již odebrány, nyní se s IRevertContainerem komunikovat nebude).
        /// Položky ale musí být schopny vygenerovat svoje data (jejich metodou IRevertItem.GetData()), protože tato data se budou budou ukládat do Reverteru.
        /// Reverter si poznamená ID a data položek.
        /// </summary>
        /// <param name="iRevertItems"></param>
        public void StoreRemove(IEnumerable<IRevertItem> iRevertItems)
        {
			if (iRevertItems == null) return;
			List<RevertOne> changes = new List<RevertOne>();
            foreach (IRevertItem iRevert in iRevertItems)
            {
				if (iRevert != null)
					this.AddRevertOne(RevertOne.CreateRemove(iRevert), changes);
            }
            this.RevertItemAdd(changes);

            this.PreparedItems = null;
        }
        /// <summary>
        /// Vymaže celý obsah reverteru
        /// </summary>
        public void Clear()
        {
            this.RevertSteps = new List<RevertStep>();
            this.StepPointer = 0;
            this.PreparedItems = null;
        }
		/// <summary>
		/// Do daného seznamu změn přidá další položku, pokud není null
		/// </summary>
		/// <param name="revertOne"></param>
		/// <param name="changes"></param>
		private void AddRevertOne(RevertOne revertOne, List<RevertOne> changes)
		{
			if (revertOne != null)
				changes.Add(revertOne);
		}
		#endregion
		#region Public metody - Undo a Redo
		/// <summary>
		/// Provede jeden krok nazpět.
		/// Pokud krok nazpět neexistuje, chybu nevyhodí.
		/// Možnost kroku zpět lze detekovat pomocí property CanUndo.
		/// </summary>
		public void Undo()
		{
			if (!this.CanUndo) return;                                  // Není žádný krok nazpět
			this.StepPointer--;
			RevertStep revertItem = this.RevertSteps[this.StepPointer]; // Předešlý stav
			revertItem.Undo(this.RevertContainer);                      // Provede si Undo sám
		}
		/// <summary>
		/// Provede jeden krok nazpět.
		/// Pokud krok nazpět neexistuje, chybu nevyhodí.
		/// Možnost kroku zpět lze detekovat pomocí property CanUndo.
		/// </summary>
		public void Redo()
		{
			if (!this.CanRedo) return;                                  // Není žádný krok vpřed
			RevertStep revertItem = this.RevertSteps[this.StepPointer]; // Následující stav
			this.StepPointer++;
			revertItem.Redo(this.RevertContainer);                      // Provede si Redo sám
		}
		#endregion
		#region Privátní metody a proměnné
		/// <summary>
		/// Přidá záznam o jedné změně.
		/// Zajišťuje přechod z režimu Undo do režimu Store (odebrání všech záznamů za aktuální pozicí StepPointer).
		/// </summary>
		/// <param name="changes"></param>
		private void RevertItemAdd(List<RevertOne> changes)
		{
			if (changes == null && changes.Count == 0)
				return;

			RevertStep revertItem = new RevertStep(changes);
			if (this.StepPointer < this.RevertSteps.Count)
				this.RevertSteps.RemoveRange(this.StepPointer, this.RevertSteps.Count - this.StepPointer);

			this.RevertSteps.Add(revertItem);
			this.StepPointer = this.RevertSteps.Count;
		}
		/// <summary>
		/// Metoda vyhledá ve zdejším privátním zásobníku this.PreparedItems objekt, jehož ID odpovídá ID daného vstupujícího objektu.
		/// </summary>
		/// <param name="iRevert"></param>
		/// <returns></returns>
		private RevertOne FindPreparedItem(IRevertItem iRevert)
		{
			if (this.PreparedItems == null) return null;
			return this.PreparedItems.FirstOrDefault(i => (i.ObjectId.CompareTo(iRevert.ObjectId) == 0));
		}
		/// <summary>
		/// Seznam prvků připravených na následující StoreChange()
		/// </summary>
        private List<RevertOne> PreparedItems;
		#endregion
        #region class RevertItem : jedna změna, více objektů
        /// <summary>
        /// Třída popisující jednu sadu změn.
        /// Typicky: pokud je vybráno 10 objektů, a naráz je jim změněna vlastnost "Text", jde o jednu změnu z hlediska uživatele.
        /// Pokud uživatel provede Undo, chce naráz vrátit změnu na všech 10 objektech.
        /// Jedna sada změn tedy obsahuje položky (RevertItemChange), popisující změny pro jednotlivé datové objekty.
        /// </summary>
        private class RevertStep
        {
            /// <summary>
            /// Konstruktor
            /// </summary>
            /// <param name="changes"></param>
            internal RevertStep(List<RevertOne> changes)
            {
                this.Changes = changes;
            }
            /// <summary>
            /// Změny provedené pro jednotlivé datové objekty.
            /// </summary>
            internal List<RevertOne> Changes { get; private set; }
			/// <summary>
			/// Záznam je prázdný = neobsahuje žádná data o změně.
			/// </summary>
			internal bool IsEmpty { get { return (this.Changes == null && this.Changes.Count == 0); } }
			/// <summary>
			/// Provede krok Undo = zajistí vložení dat Old do objektů skrze předaný IRevertContainer
			/// </summary>
			/// <param name="revertContainer"></param>
			internal void Undo(IRevertContainer revertContainer)
			{
				if (revertContainer == null) return;
				foreach (RevertOne item in this.Changes)
				{
					IRevertItem iRevert;
					switch (item.Change)
					{
						case RevertChangeType.Add:
							// Charakter změny je: "Byl přidán nový prvek". Akce Undo => odebrat prvek:
							revertContainer.RemoveItem(item.ObjectId);
							break;
						case RevertChangeType.Modify:
							// Charakter změny je: "Byl změněn prvek". Akce Undo => vložit do prvku data Old:
							iRevert = revertContainer.GetItem(item.ObjectId);
							if (iRevert != null)
								iRevert.SetData(item.DataOld);
							break;
						case RevertChangeType.Remove:
							// Charakter změny je: "Byl odebrán prvek". Akce Undo => obnovit jej do stavu Old:
							iRevert = revertContainer.CreateItem(item.ObjectId, item.DataOld);
							if (iRevert != null)
								iRevert.SetData(item.DataOld);
							break;
					}
				}
			}
			/// <summary>
			/// Provede krok Redo = zajistí vložení dat New do objektů skrze předaný IRevertContainer
			/// </summary>
			/// <param name="revertContainer"></param>
			internal void Redo(IRevertContainer revertContainer)
			{
                if (revertContainer == null) return;
                foreach (RevertOne item in this.Changes)
                {
                    IRevertItem iRevert;
                    switch (item.Change)
                    {
                        case RevertChangeType.Add:
                            // Charakter změny je: "Byl přidán nový prvek". Akce Redo => zase ten prvek přidáme:
                             iRevert = revertContainer.CreateItem(item.ObjectId, item.DataNew);
                            if (iRevert != null)
                                iRevert.SetData(item.DataNew);
                            break;
                        case RevertChangeType.Modify:
                            // Charakter změny je: "Byl změněn prvek". Akce Redo  => vložit do prvku data New:
                            iRevert = revertContainer.GetItem(item.ObjectId);
                            if (iRevert != null)
                                iRevert.SetData(item.DataNew);
                            break;
                        case RevertChangeType.Remove:
                            // Charakter změny je: "Byl odebrán prvek". Akce Redo => zase ten prvek odebereme:
                            revertContainer.RemoveItem(item.ObjectId);
                            break;
                    }
                }
            }
		}
        #endregion
        #region class RevertItemChange : jedna změna jednoho objektu
        /// <summary>
        /// Třída popisující jednu změnu jednoho datového prvku.
        /// </summary>
        private class RevertOne
        {
			#region Konstrukce
			/// <summary>
			/// Privátní konstruktor
			/// </summary>
			private RevertOne() { }
			/// <summary>
			/// Vrátí položku pro změnu typu Add
			/// </summary>
			/// <param name="iRevert"></param>
			/// <returns></returns>
			internal static RevertOne CreateAdd(IRevertItem iRevert)
			{
				RevertOne item = new RevertOne();
				item.ObjectId = iRevert.ObjectId;
				item.DataOld = null;
				item.Change = RevertChangeType.Add;
				item.DataNew = iRevert.GetData();
				return item;
			}
			/// <summary>
			/// Vrátí položku pro změnu typu Modify, Prepare
			/// </summary>
			/// <param name="iRevert"></param>
			/// <returns></returns>
			internal static RevertOne CreateModifyBefore(IRevertItem iRevert)
			{
				RevertOne item = new RevertOne();
				item.ObjectId = iRevert.ObjectId;
				item.DataOld = iRevert.GetData();
				item.Change = RevertChangeType.None;
				item.DataNew = null;
				return item;
			}
			/// <summary>
			/// Vrátí položku pro změnu typu Modify, After change.
			/// Pozor: současně do objektu preparedItem do jeho DataOld vloží aktuální data objektu iRevert !
			/// </summary>
			/// <param name="preparedItem"></param>
			/// <param name="iRevert"></param>
			/// <returns></returns>
			internal static RevertOne CreateModifyAfter(RevertOne preparedItem, IRevertItem iRevert)
			{
				if (preparedItem == null || iRevert == null) return null;

				string dataNew = iRevert.GetData();
				if (preparedItem.DataOld == dataNew) return null;

				RevertOne item = new RevertOne();
				item.ObjectId = iRevert.ObjectId;
				item.DataOld = preparedItem.DataOld;
				item.Change = RevertChangeType.Modify;
				item.DataNew = dataNew;

				preparedItem.DataOld = dataNew;

				return item;
			}
			/// <summary>
			/// Vrátí položku pro změnu typu Remove
			/// </summary>
			/// <param name="iRevert"></param>
			/// <returns></returns>
			internal static RevertOne CreateRemove(IRevertItem iRevert)
			{
				RevertOne item = new RevertOne();
				item.ObjectId = iRevert.ObjectId;
				item.DataOld = iRevert.GetData();
				item.Change = RevertChangeType.Remove;
				item.DataNew = null;
				return item;
			}
			/// <summary>
			/// Vrátí položku pro změnu typu Remove
			/// </summary>
			/// <param name="itemChange"></param>
			/// <returns></returns>
			internal static RevertOne CreateRemove(RevertOne itemChange)
			{
				RevertOne item = new RevertOne();
				item.ObjectId = itemChange.ObjectId;
				item.DataOld = itemChange.DataOld;
				item.Change = RevertChangeType.Remove;
				item.DataNew = null;
				return item;
			}
			#endregion
			#region Public property
			/// <summary>
            /// Identifikace objektu.
            /// Přichází s objektem, slouží k získání objektu při Undo/Redo.
            /// Uvnitř Reverteru se nijak nevyužívá.
            /// </summary>
            internal IComparable ObjectId { get; private set; }
            /// <summary>
			/// Data objektu před změnou
            /// </summary>
            internal string DataOld { get; private set; }
            /// <summary>
            /// Druh změny
            /// </summary>
            internal RevertChangeType Change { get; private set; }
            /// <summary>
            /// Data objektu po změně
            /// </summary>
            internal string DataNew { get; private set; }
			#endregion
		}
        #endregion
    }
    #endregion
    #region enum ChangeType : druh změny
    /// <summary>
    /// Druh změny nad objektem
    /// </summary>
    public enum RevertChangeType
    {
        /// <summary>Není změna</summary>
        None = 0,
        /// <summary>Založení nového objektu</summary>
        Add,
        /// <summary>Změna existujícího objektu</summary>
        Modify,
        /// <summary>Odebrání existujícího objektu</summary>
        Remove
    }
    #endregion
	#region interface IRevertContainer, IRevertItem
    /// <summary>
    /// Interface zaručující schopnost datového containeru (který obsahuje sady datových objektů) realizovat akce vyvolávané Reverterem.
    /// Jde o akce podporující Undo a Redo: Vytvořit a vrátit nový objekt daného typu, Najít a vrátit objekt podle jeho ID pro provedení změn, odebrat objekt podle jeho ID.
    /// </summary>
    public interface IRevertContainer
    {
        /// <summary>
        /// Metoda má za úkol vytvořit a vrátit objekt daného ID, vyhovující daným datům.
        /// Objekt nemusí být v rámci této metody naplněn daty (objectData), 
        /// data do objektu budou vložena ihned po jeho vrácení z této metody (IRevertContainer.CreateItem()).
        /// Pokud bude vrácen objekt null, k chybě nedojde.
        /// </summary>
        /// <param name="objectId"></param>
        /// <param name="objectData"></param>
        /// <returns></returns>
        IRevertItem CreateItem(IComparable objectId, string objectData);
        /// <summary>
        /// Metoda má za úkol najít a vrátit objekt daného ID.
        /// Do vráceného objektu budou následně vkládána data.
        /// Pokud bude vrácen objekt null, k chybě nedojde.
        /// </summary>
        /// <param name="objectId"></param>
        /// <returns></returns>
        IRevertItem GetItem(IComparable objectId);
        /// <summary>
        /// Metoda má za úkol najít a odstranit objekt daného ID.
        /// </summary>
        /// <param name="objectId"></param>
        void RemoveItem(IComparable objectId);
    }
	/// <summary>
	/// Interface zaručující schopnost jednotlivého objektu provádět Undo/Redo na základě uložení jeho serializovaných dat (do Reverteru),
	/// a později jejich zpětnému vkládání do objektu (Undo + Redo)
	/// </summary>
	public interface IRevertItem
	{
		/// <summary>
		/// ID objektu. Slouží k jeho identifikaci mezi metodami Reverteru a objektem IRevertContainer.
		/// </summary>
		IComparable ObjectId { get; }
		/// <summary>
		/// Datový objekt vrátí stringová data, popisující jeho aktuální stav.
		/// Je voláno v případě, kdy Reverter sbírá data o objektu pro jejich uchování ve své paměti.
		/// </summary>
		/// <returns></returns>
		string GetData();
		/// <summary>
		/// Datový objekt převezme stringová data, a podle nich upraví svůj aktuální stav.
		/// Je voláno v případě, kdy Reverter provádí Undo nebo Redo, a objekt se má nastavit do stavu odpovídajícímu danému stavu.
		/// </summary>
		/// <param name="data"></param>
		void SetData(string data);
	}
	#endregion
}
